using System;
using System.Collections.Generic;
using System.Linq;
using Magiq.Behaviors;
using Magiq.Support;
using Magiq.Update;

namespace Magiq.Providers.Objects
{
    public class UpdateStrategy<T> : BehaviorHandler<IObjectsUpdateBehavior>, IUpdateStrategy where T : class
    {
        private readonly Dictionary<IPropertySet, Delegate> compiledChains = new Dictionary<IPropertySet, Delegate>();

        private readonly Dictionary<IPropertySet, Delegate> compiledSettersValues =
            new Dictionary<IPropertySet, Delegate>();

        private readonly IUpdate<T> update;

        public UpdateStrategy(IUpdate<T> update) : base(update.Behavior)
        {
            this.update = update;
            Default<DefaultObjectsBehavior>();
        }

        #region IUpdateStrategy Members

        public int Execute()
        {
            CompileProperties();

            var updates = new List<object>();

            foreach (var item in update.Source)
            {
                foreach (var set in update.PropertySets)
                {
                    var returnType = set.ValueExpression.Body.Type;

                    object value = null;
                    bool valueGetted;

                    var candidateValue = Call(x => x.GetValue(item, set, compiledSettersValues[set]));

                    if (set.PropertyType == returnType)
                    {
                        try
                        {
                            value = candidateValue;
                            valueGetted = true;
                        }
                        catch (NullReferenceException)
                        {
                            valueGetted = Call(x => x.ShouldUpdateWhenValueIsNull(item, set));

                            if (valueGetted)
                                value = Call(x => x.ValueForNull(item, set));
                        }
                    }
                    else
                    {
                        if (set.PropertyType.IsGenericType &&
                            set.PropertyType.GetGenericTypeDefinition() == typeof (Nullable<>))
                            returnType = set.PropertyType.GetGenericArguments().First();

                        try
                        {
                            value = set.PropertyType == returnType
                                        ? candidateValue
                                        : Convert.ChangeType(candidateValue, set.PropertyType);
                            valueGetted = true;
                        }
                        catch (NullReferenceException)
                        {
                            valueGetted = Call(x => x.ShouldUpdateWhenValueIsNull(item, set));

                            if (valueGetted)
                                value = Call(x => x.ValueForNull(item, set));
                        }
                    }

                    if (!valueGetted) continue;

                    Delegate chain;
                    var changed = false;

                    var itemToUpdate = compiledChains.TryGetValue(set, out chain)
                                           ? chain.DynamicInvoke(item)
                                           : item;

                    if (itemToUpdate != null)
                    {
                        Call(x => x.SetProperty(itemToUpdate, set.PropertyName, value));
                        changed = true;
                    }

                    if (changed && !updates.Contains(itemToUpdate))
                        updates.Add(itemToUpdate);
                }
            }
            return updates.Count;
        }

        #endregion

        private void CompileProperties()
        {
            foreach (var set in update.PropertySets)
            {
                compiledSettersValues.Add(set, set.ValueExpression.Compile());

                var expression = set.PropertyExpression.PreviousExpressionChain();

                if (expression != null)
                    compiledChains.Add(set, expression.Compile());
            }
        }
    }
}